# 一、什么是跨域
跨域是指,由于浏览器的同源策略限制,将会阻止一个域的脚本与另一个域的内容获取及交互操作。
同源是指协议、域名、端口均相等,同源策略是浏览器安全机制的基础。常见的同源限制包括:
- 无法读取非同源网页的 cookie、localStorage、sessionStorage、indexedDB;
- 无法获取非同源网页的 DOM;
- 无法向非同源地址发送 Ajax 请求(其实请求可以成功发出,只是同源策略禁止读取跨域地址返回的响应)。
其实,同源策略的本质是,一个域名下的 js,在未经允许的情况下,不得读取另一个域名的内容(也就是不给其它域操控当前域的机会,从而确保自身安全)。但是并不阻止向另一域名发送内容。
这也就是为什么 from 表单可以跨域提交数据,而 ajax 却不能完成跨域请求。
(from 表单提交后,是不会有任何数据返回,也就不会读到另一个域的数据,就不会受到同源策略的限制。而 ajax 发送跨域请求后,是一定会返回响应报文的,也就会读到另一个域的数据,这就违背了同源策略的限制。)
# 二、如何实现跨域通信
常用方案:
- 1. jsonp
- 2. CORS
- 3. WebSocket
- 4. document.domain + iframe
- 5. location.hash + iframe
- 6. window.name + iframe
- 7. postMessage + iframe
- 8. nginx代理
- 9. nodejs中间件代理
# 1. jsonp
实现原理:浏览器允许三个html标签跨域加载资源,分别是:link、script、img。基于此特性,可以通过动态创建 script 标签,加载一个带有回调函数名称的地址,实现跨域通信。
(1) 原生实现
var script = document.createElement('script');
script.src = 'http://www.jerryzhang.com:8080/login?user=admin&callback=handleCallback';
document.body.appendChild(script);
// 回调执行函数
function handleCallback(res) {
alert(JSON.stringify(res));
}
服务端返回数据如下:
handleCallback({"status": true, "user": "admin"})
前端获取服务端返回的数据,即可执行预先定义的回调函数,拿到参数中的数据。
(2) jquery ajax
ajax 本身只能同源使用,而 ajax 的 jsonp 只是对动态创建 script 标签实现 jsonp 的封装。
$.ajax({
url:'http://www.jerryzhang.com/login',
type:'GET',
dataType:'jsonp', // 请求方式为 jsonp,此时 ajax 与 XMLHttpRequest 无关,只是对动态创建 script 标签实现 jsonp 的封装。
jsonpCallback:'callback',
data:{
"username":"jerryzhang"
}
})
jsonp 缺陷:
- 仅支持跨域 http/https 请求,不支持跨域 js 交互(如获取另一个域中的 DOM 节点);
- 仅支持 get 请求 (为什么? 因为:jsonp 的实现原理核心是 script 标签,而 script 仅支持 get 方式加载资源,所以 jsonp 仅支持 get!)
若需要支持 get 之外的其它 http/https 跨域请求,可选择 CORS。
# 2. CORS
实现原理:
CORS
全称“跨域资源共享”(Cross-origin resource sharing),能实现任意方式(get、post、put等)的 http/https 请求。
关于CORS
的更多细节,可参考阮一峰的跨域资源共享 CORS 详解 (opens new window)。
对于前端开发者来说,CORS
通信与同源AJAX
通信没有区别,浏览器一旦发现AJAX
请求跨域,就会自动添加一些附加的请求头信息(非简单请求时还会多发一次预检请求)。目前,IE10+及主流浏览器都支持CORS
。CORS
通信的关键是服务端实现支持CORS
的接口。
CORS
请求分为两种:简单请求和非简单请求。浏览器对两种请求的处理不同。
同时满足以下两个条件的,就是简单请求:
- 请求方法为
HEAD
、GET
、POST
中任意一种; - 头信息中只包含
Accept
、Accept-Language
、Content-Language
、Last-Event-ID
、Content-Type(值为application/x-www-form-urlencoded、multipart/form-data、text/plain)
。
不满足上述两个条件的,就是非简单请求。
2.1 简单请求
基本流程
当浏览器发现这次跨域AJAX
请求为简单请求时,就自动在头信息之中,添加一个Origin
字段,用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。
2.2 非简单请求 常见非简单请求
- put、delete方法的ajax请求
- 请求字段为 json 格式(Content-type: application/json)的 post 请求
- 带自定义头字段的ajax请求,假设带自定义字段 x-custom-header
对应解决方法(均为服务端设置):
- access-control-allow-methods: PUT,DELETE
- access-control-allow-header: Content-type
- access-control-allow-header: x-custom-header
# 3. WebSocket
实现原理:
WebSocket
实现了浏览器与服务器的全双工通信,同时允许跨域通信,是seaver push
技术的很好实现。
// 前端代码
var ws = new WebSocket("wss://echo.websocket.org");
ws.onopen = function(evt) {
console.log("Connection open ...");
ws.send("Hello WebSockets!");
};
ws.onmessage = function(evt) {
console.log( "Received Message: " + evt.data);
ws.close();
};
ws.onclose = function(evt) {
console.log("Connection closed.");
};
WebSocket
通信需要服务端配合实现。
# 4. document.domain + iframe
此方案仅限于主域名相同,子域名不同的跨域场景。eg: http://www.jerryzhang.com/a.html 与 http://child.jerryzhang.com/b.html 主域名都是 jerryzhang.com,子域名分别为 www 和 child。
实现原理:通过将两个页面的 document.domain 设置成相同的域名(只能设置为公共主域名),人为的实现同域,避免了跨域。
父页面:(http://www.jerryzhang.com/a.html)
<iframe id="iframe" src="http://child.jerryzhang.com/b.html"></iframe>
<script>
document.domain = 'jerryzhang.com' // 设置成相同的域名(只能设置为公共主域名)
var fatherName = 'jerry'
console.log(document.getElementById('iframe').contentWindow.childName) // 'child'
</script>
子页面:(http://child.jerryzhang.com/a.html)
<script>
document.domain = 'jerryzhang.com' // 设置成相同的域名(只能设置为公共主域名)
var childName = 'child'
console.log(window.parent.fatherName) // 'jerry'
</script>
缺陷:
- 仅适用于主域名相同,子域名不同的跨域场景。
# 5. location.hash + iframe
实现原理:父页面与 iframe 页面可以读写彼此的 url,而 url 中的 hash 不参与实际 http 请求,且 onhashchange 事件可以监听到 hash 的变化,所以可以将 hash 作为跨域通信的桥梁。
假设父页面为 http://baidu.com/a.html ,iframe 页面为 http://google.com/b.html
(1) 父页面向 iframe 页面传送数据
// 父页面 a.html
<iframe id="iframe" src="http://google.com/b.html"></iframe>
<script>
var iframe = document.getElementById('iframe')
iframe.onload = function() {
var data = '1010101'
iframe.contentWindow.location.hash = data
}
</script>
// iframe 页面 b.html
<script>
window.addEventListener('hashchange', (data) => {
console.log(location.hash) // '#1010101'
})
</script>
(2) iframe 页面向父页面传送数据
由于跨域情况下,IE、Chrome 不允许直接修改 parent.location.hash,所以需借助一个与父页面同域的隐藏 iframe 页面,作为中间过渡,实现从子向父的数据传递。
// 父页面 a.html
<iframe id="iframe" src="http://google.com/b.html"></iframe>
<script>
window.addEventListener('hashchange', (data) => {
console.log(location.hash) // '#1010101'
})
</script>
// iframe 页面 b.html
<script>
const data = '1010101'
try {
parent.location.hash = data // 非 IE、Chrome 可直接修改 parent.location.hash
} catch(e) {
// 创建与父页面 a.html 同域的隐藏的代理 iframe 页面 proxy.html
var ifrproxy = document.createElement('iframe')
ifrproxy.style.display = 'none'
ifrproxy.src = 'http://baidu.com/proxy.html#data' // proxy.html 作为代理页面,先接收子页面的数据,再传递给父页面
docuemnt.body.appendChild(ifrproxy)
}
</script>
// 隐藏的代理 iframe 页面 proxy.html,将 b.html 传递来的 data,再传给真正的父页面 a.html
<script>
parent.parent.location.hash = location.hash.substring(1)
</script>
# 6. window.name + iframe
# 7. postMessage + iframe
实现原理:
postMessage()
方法提供了一种受控的可以规避同源策略实现跨域通信的机制。IE10及以上
语法:
targetWindow.postMessage(message, targetOrigin) // 在目标页面`targetWindow`向目标源`targetOrigin`发送消息`message`
`targetWindow` 目标页面的引用,例如 iframe 页面的 contentWindow 属性。
`message` 要发送到目标源`targetOrigin`的消息。若为对象需序列化成字符串。
`targetOrigin` 目标页面`targetWindow`对应的域名。
假设父页面为 http://baidu.com/a.html ,iframe 页面为 http://google.com/b.html
// 父页面 a.html
<iframe id="iframe" src="http://google.com/b.html"></iframe>
<script>
// 向 b.html 发送数据
var targetWindow = document.getElementById('iframe')
var targetOrigin = 'http://google.com'
targetWindow.postMessage('Hello b.html!', targetOrigin)
// 接收 a.html 发来的数据
window.addEventListener('message', (data) => { // 监听 message 事件,获取数据
console.log(data) // 'Hello a.html!'
})
</script>
// iframe 页面 b.html
<script>
window.addEventListener('message', (data) => { // 监听 message 事件,获取数据
console.log(data) // 'Hello b.html!'
// 向 a.html 发送数据
var targetOrigin = 'http://baidu.com'
window.parent.postMessage('Hello a.html!', targetOrigin)
})
</script>
安全问题:如果不希望你的网站接收来自别的网站的数据(比如跨域攻击),请确保不要为 message 事件添加监听器。
# 8. nginx代理
# 9. nodejs中间件代理
← 记一次性能排查及解决方案 页面埋点实践 →